From: Jonas Ã…dahl Date: Fri, 3 Oct 2025 15:02:37 +0000 (+0200) Subject: status/keyboard: Adapt to external source of keyboard layout X-Git-Tag: archive/raspbian/50.5-1+rpi1~1^2~39^2~1^2~32 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=0f90346872477d91a3e0be605a55bca1d163d735;p=gnome-shell.git status/keyboard: Adapt to external source of keyboard layout This adapts to API changes that allow an external source (e.g. remote desktop API user) to control the current keyboard layout. This is done in two ways: If the external keyboard source sets the keymap as "locked", the indicator is fixed to the short name of the external source, and will change the indicator label when the layout index is changed accordingly. The builtin input sources are not selectable, and the keyboard selection keybinding is disabled. If the external keyboard source doesn't set the keymap as "locked", whenever the current keymap is from an external source, it's indicated as a non-sensitive but selected input source. Selecting another keyboard layout overrides the one selected by the external entity. Part-of: --- diff --git a/js/misc/keyboardManager.js b/js/misc/keyboardManager.js index 924caa8c8..5724156e1 100644 --- a/js/misc/keyboardManager.js +++ b/js/misc/keyboardManager.js @@ -1,5 +1,8 @@ import GLib from 'gi://GLib'; import GnomeDesktop from 'gi://GnomeDesktop'; +import Meta from 'gi://Meta'; + +import * as Signals from './signals.js'; export const DEFAULT_LOCALE = 'en_US'; export const DEFAULT_LAYOUT = 'us'; @@ -27,8 +30,10 @@ export function getKeyboardManager() { return _keyboardManager; } -class KeyboardManager { +class KeyboardManager extends Signals.EventEmitter { constructor() { + super(); + // The XKB protocol doesn't allow for more than 4 layouts in a // keymap. Wayland doesn't impose this limit and libxkbcommon can // handle up to 32 layouts but since we need to support X clients @@ -40,11 +45,18 @@ class KeyboardManager { this._localeLayoutInfo = this._getLocaleLayout(); this._layoutInfos = {}; this._currentKeymap = null; + + global.backend.connect('keymap-changed', this._onKeymapChanged.bind(this)); + global.backend.connect('keymap-layout-group-changed', this._onKeymapLayoutGroupChanged.bind(this)); + global.backend.connect('reset-keymap-description', + () => this._ourKeymapDescription); + global.backend.connect('reset-keymap-layout-index', + () => this._current.groupIndex); } - async _applyLayoutGroup(group) { + _updateCurrentKeymap(info) { const options = this._buildOptionsString(); - const [layouts, variants] = this._buildGroupStrings(group); + const [layouts, variants] = this._buildGroupStrings(info.group); const model = this._xkbModel; if (this._currentKeymap && @@ -52,36 +64,68 @@ class KeyboardManager { this._currentKeymap.variants === variants && this._currentKeymap.options === options && this._currentKeymap.model === model) - return; + return false; + + const displayNames = info.group.map(g => g.displayName); + const shortNames = info.group.map(g => g.shortName); + this._currentKeymap = { + layouts, + variants, + options, + model, + displayNames, + shortNames, + }; + return true; + } - this._currentKeymap = {layouts, variants, options, model}; - await global.backend.set_keymap_async(layouts, variants, options, model, null); + _createKeymapDescription() { + return Meta.KeymapDescription.new_from_rules(this._currentKeymap.model, + this._currentKeymap.layouts, + this._currentKeymap.variants, + this._currentKeymap.options, + this._currentKeymap.displayNames, + this._currentKeymap.shortNames + ); } - async _applyLayoutGroupIndex(idx) { - await global.backend.set_keymap_layout_group_async(idx, null); + _onKeymapChanged() { + this._keymapDescription = global.backend.get_keymap_description(); + this.emit('keymap-changed'); } - async _doApply(info) { - await this._applyLayoutGroup(info.group); - await this._applyLayoutGroupIndex(info.groupIndex); + _onKeymapLayoutGroupChanged() { + this.emit('keymap-changed'); } - apply(id) { + async _doApply(id) { const info = this._layoutInfos[id]; if (!info) return; - if (this._current && this._current.group === info.group) { - if (this._current.groupIndex !== info.groupIndex) - this._applyLayoutGroupIndex(info.groupIndex).catch(logError); - } else { - this._doApply(info).catch(logError); + let recreate; + if (this._updateCurrentKeymap(info)) + recreate = true; + else if (this.isExternal()) + recreate = true; + else + recreate = false; + + if (recreate) + this._ourKeymapDescription = this._createKeymapDescription(); + + if (recreate || !this._current || this._current.groupIndex !== info.groupIndex) { + await global.backend.set_keymap_async( + this._ourKeymapDescription, info.groupIndex, null); } this._current = info; } + apply(id) { + this._doApply(id).catch(logError); + } + reapply() { if (!this._current) return; @@ -94,9 +138,17 @@ class KeyboardManager { this._layoutInfos = {}; for (const id of ids) { - const [found, , , layout, variant] = this._xkbInfo.get_layout_info(id); - if (found) - this._layoutInfos[id] = {id, layout, variant}; + const [found, displayName, shortName, layout, variant] = + this._xkbInfo.get_layout_info(id); + if (found) { + this._layoutInfos[id] = { + id, + layout, + variant, + displayName, + shortName, + }; + } } let i = 0; @@ -160,4 +212,28 @@ class KeyboardManager { get currentLayout() { return this._current; } + + get shortName() { + const seat = global.stage.context.get_backend().get_default_seat(); + const keymap = seat.get_keymap(); + return keymap.get_current_short_name(); + } + + get displayName() { + const seat = global.stage.context.get_backend().get_default_seat(); + const keymap = seat.get_keymap(); + return keymap.get_current_display_name(); + } + + isLocked() { + return this._keymapDescription?.is_locked() ?? false; + } + + isExternal() { + if (!this._keymapDescription) + return false; + if (!this._ourKeymapDescription) + return true; + return !this._keymapDescription.direct_equal(this._ourKeymapDescription); + } } diff --git a/js/ui/status/keyboard.js b/js/ui/status/keyboard.js index 600a1cf57..58b481336 100644 --- a/js/ui/status/keyboard.js +++ b/js/ui/status/keyboard.js @@ -25,8 +25,6 @@ class LayoutMenuItem extends PopupMenu.PopupBaseMenuItem { _init(displayName, shortName) { super._init(); - this.setOrnament(PopupMenu.Ornament.NO_DOT); - this.label = new St.Label({ text: displayName, x_expand: true, @@ -371,6 +369,7 @@ export class InputSourceManager extends Signals.EventEmitter { this._xkbInfo = KeyboardManager.getXkbInfo(); this._keyboardManager = KeyboardManager.getKeyboardManager(); + this._keyboardManager.connect('keymap-changed', this._keymapChanged.bind(this)); this._ibusReady = false; this._ibusManager = IBusManager.getIBusManager(); @@ -426,6 +425,9 @@ export class InputSourceManager extends Signals.EventEmitter { } _switchInputSource(display, window, event, binding) { + if (this._keyboardManager.isLocked()) + return; + if (this._mruSources.length < 2) return; @@ -459,6 +461,10 @@ export class InputSourceManager extends Signals.EventEmitter { this._keyboardManager.reapply(); } + _keymapChanged() { + this.emit('keymap-changed'); + } + _updateMruSettings() { // If IBus is not ready we don't have a full picture of all // the available sources, so don't update the setting @@ -888,7 +894,8 @@ class InputSourceIndicator extends PanelMenu.Button { this._inputSourceManager = getInputSourceManager(); this._inputSourceManager.connectObject( 'sources-changed', this._sourcesChanged.bind(this), - 'current-source-changed', this._currentSourceChanged.bind(this), this); + 'current-source-changed', this._currentSourceChanged.bind(this), + 'keymap-changed', this._keymapChanged.bind(this), this); this._inputSourceManager.reload(); } @@ -904,47 +911,114 @@ class InputSourceIndicator extends PanelMenu.Button { this._showLayoutItem.visible = Main.sessionMode.allowSettings; } - _sourcesChanged() { - for (const i in this._menuItems) - this._menuItems[i].destroy(); - for (const i in this._indicatorLabels) - this._indicatorLabels[i].destroy(); + _createExternalSource(keyboardManager) { + const {displayName, shortName} = keyboardManager; + const is = new InputSource('external', 'external', displayName, shortName, -1); + is.locked = keyboardManager.isLocked(); + return is; + } - this._menuItems = {}; - this._indicatorLabels = {}; + _getCurrentSource() { + const {keyboardManager} = this._inputSourceManager; + if (keyboardManager.isExternal()) + return this._inputSources[0]; + else + return this._inputSourceManager.currentSource; + } + + _updateInputSources() { + const {keyboardManager} = this._inputSourceManager; + if (keyboardManager.isLocked()) { + const is = this._createExternalSource(keyboardManager); + this._inputSources = [is]; + return; + } - let menuIndex = 0; - for (const i in this._inputSourceManager.inputSources) { - const is = this._inputSourceManager.inputSources[i]; + const inputSources = []; + if (keyboardManager.isExternal()) + inputSources.push(this._createExternalSource(keyboardManager)); + + const internalSources = this._inputSourceManager.inputSources; + inputSources.push( + ...Object.keys(internalSources).sort((a, b) => a - b).map(i => internalSources[i])); + + this._inputSources = inputSources; + } + + _addSourceIndicators() { + const inputSources = this._inputSources; + for (const i in inputSources) { + const is = inputSources[i]; const menuItem = new LayoutMenuItem(is.displayName, is.shortName); - menuItem.connect('activate', () => is.activate(true)); + if (is.type !== 'external') + menuItem.connect('activate', () => is.activate(true)); + else + menuItem.sensitive = false; + + if (is.locked) + menuItem.setOrnament(PopupMenu.Ornament.HIDDEN); + else + menuItem.setOrnament(PopupMenu.Ornament.NO_DOT); const indicatorLabel = new St.Label({ text: is.shortName, visible: false, }); - this._menuItems[i] = menuItem; - this._indicatorLabels[i] = indicatorLabel; + const indicatorIndex = this._calculateSourceIndicatorIndex(is); + + this._menuItems[indicatorIndex] = menuItem; + this._indicatorLabels[indicatorIndex] = indicatorLabel; is.connect('changed', () => { menuItem.indicator.set_text(is.shortName); indicatorLabel.set_text(is.shortName); }); - this.menu.addMenuItem(menuItem, menuIndex++); + this.menu.addMenuItem(menuItem, indicatorIndex); this._container.add_child(indicatorLabel); } } + _sourcesChanged() { + this._updateInputSources(); + + for (const i in this._menuItems) + this._menuItems[i].destroy(); + for (const i in this._indicatorLabels) + this._indicatorLabels[i].destroy(); + + this._menuItems = {}; + this._indicatorLabels = {}; + + this._addSourceIndicators(); + } + + _calculateSourceIndicatorIndex(source) { + const keyboardManager = this._inputSourceManager.keyboardManager; + return (keyboardManager.isExternal() ? 1 : 0) + source.index; + } + + _setSourceAsActive(source) { + const index = this._calculateSourceIndicatorIndex(source); + if (!source.locked) + this._menuItems[index]?.setOrnament(PopupMenu.Ornament.DOT); + this._indicatorLabels[index]?.show(); + } + + _setSourceAsInactive(source) { + const index = this._calculateSourceIndicatorIndex(source); + if (!source.locked) + this._menuItems[index]?.setOrnament(PopupMenu.Ornament.NO_DOT); + this._indicatorLabels[index].hide(); + } + _currentSourceChanged(manager, oldSource) { - const nVisibleSources = Object.keys(this._inputSourceManager.inputSources).length; - const newSource = this._inputSourceManager.currentSource; + const nVisibleSources = Object.keys(this._inputSources).length; + const newSource = this._getCurrentSource(); - if (oldSource) { - this._menuItems[oldSource.index].setOrnament(PopupMenu.Ornament.NO_DOT); - this._indicatorLabels[oldSource.index].hide(); - } + if (oldSource) + this._setSourceAsInactive(oldSource); if (!newSource || (nVisibleSources < 2 && !newSource.properties)) { // This source index might be invalid if we weren't able @@ -962,8 +1036,12 @@ class InputSourceIndicator extends PanelMenu.Button { this._buildPropSection(newSource.properties); - this._menuItems[newSource.index].setOrnament(PopupMenu.Ornament.DOT); - this._indicatorLabels[newSource.index].show(); + this._setSourceAsActive(newSource); + } + + _keymapChanged() { + this._sourcesChanged(); + this._setSourceAsActive(this._getCurrentSource()); } _buildPropSection(properties) { @@ -1005,9 +1083,10 @@ class InputSourceIndicator extends PanelMenu.Button { else text = prop.get_label().get_text(); - const currentSource = this._inputSourceManager.currentSource; + const currentSource = this._getCurrentSource(); if (currentSource) { - const indicatorLabel = this._indicatorLabels[currentSource.index]; + const index = this._calculateSourceIndicatorIndex(currentSource); + const indicatorLabel = this._indicatorLabels[index]; const graphemeClusters = this._getGraphemeClusters(text); if (graphemeClusters.length > 0 && graphemeClusters.length < 3) indicatorLabel.set_text(text);